// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package base

import (
	"fmt"
	"regexp"
	"runtime/debug"
	"strconv"
	"strings"
)

var (
	regexStackFile               = regexp.MustCompile(`^([^:]+):(\d+)`)
	regexStackCall               = regexp.MustCompile(`\(([^)]*)\)$`)
	regexStackCallIsLogInternal  = regexp.MustCompile(`/go-wares/log.(Fatal|Panic)[fc]*\(\)$`)
	regexStackCallIsSpanInternal = regexp.MustCompile(`/go-wares/log/tracer.\(\*span\).(Fatal|Panic)\(\)$`)
	regexStackCallIsPanic        = regexp.MustCompile(`^panic\(\)$`)
)

type (
	// Stack
	// is a component for call stack.
	Stack interface {
		String() string
	}

	// StackItem
	// is a component for call stack item.
	StackItem struct {
		Callee string
		Line   int
		Path   string
	}

	stack struct {
		isPanic bool
		items   []*StackItem
	}
)

// String
// returns the stack string with mul-lines.
func (o *stack) String() string {
	list := make([]string, 0)
	for i, item := range o.items {
		list = append(list, item.Render(i))
	}
	return strings.Join(list, "\n")
}

// +---------------------------------------------------------------------------+
// | Access methods                                                            |
// +---------------------------------------------------------------------------+

func (o *stack) init() *stack {
	o.items = make([]*StackItem, 0)
	o.parse()
	return o
}

func (o *stack) parse() {
	var (
		ls    = strings.Split(strings.TrimSpace(string(debug.Stack())), "\n")
		num   = len(ls)
		items = make([]*StackItem, 0)
	)

	// Iterate and parse stack.
	for i := num - 1; i > 0; i -= 2 {
		file := strings.TrimSpace(ls[i])

		// Check line number of the stack.
		m := regexStackFile.FindStringSubmatch(file)
		if len(m) != 3 {
			break
		}

		// Generate runtime function.
		call := regexStackCall.ReplaceAllString(strings.TrimSpace(ls[i-1]), "()")

		// Match panic call.
		if regexStackCallIsPanic.MatchString(call) {
			o.isPanic = true
			break
		}

		// Match internal callee.
		if regexStackCallIsLogInternal.MatchString(call) || regexStackCallIsSpanInternal.MatchString(call) {
			break
		}

		// Append to stack.
		items = append(items, (&StackItem{
			Path: m[1], Callee: call,
		}).Parse(m[2]))
	}

	// Reverse items.
	for i := len(items) - 1; i >= 0; i-- {
		o.items = append(o.items, items[i])
	}
}

func (o *StackItem) Parse(s string) *StackItem {
	o.Line, _ = strconv.Atoi(s)
	return o
}

func (o *StackItem) Render(i int) string {
	return fmt.Sprintf(`#%d %s at %s:%d`,
		i, o.Callee, o.Path, o.Line,
	)
}
